package com.citrix.gs.util;

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.dom4j.Document;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.citrix.gs.kit.PerforceKit;
import com.citrix.gs.kit.PerforcePasswordEncryptor;
import com.citrix.gs.util.beacon.BeaconCI;
import com.citrix.gs.util.beacon.BeaconJobType;
import com.citrix.gs.util.beacon.Perforce;
import com.citrix.gs.util.beacon.Repository;
import com.citrix.gs.util.beacon.ToolType;
import com.citrix.gs.util.beacon.job.BeaconConfigJob;
import com.citrix.gs.util.beacon.job.BeaconDataImportJob;
import com.citrix.gs.util.beacon.job.BeaconJob;
import com.citrix.gs.util.beacon.job.BeaconSrcJob;
import com.citrix.gs.util.beacon.job.BeaconToolJob;
import com.citrix.gs.util.beacon.job.BeaconToolsetJob;
import com.citrix.gs.util.beacon.job.Deployable;
import com.citrix.gs.util.common.CIInfo;
import com.citrix.gs.util.common.Config;
import com.offbytwo.jenkins.model.BuildResult;
import com.perforce.p4java.core.file.FileSpecBuilder;
import com.perforce.p4java.core.file.FileSpecOpStatus;
import com.perforce.p4java.core.file.IFileSpec;
import com.perforce.p4java.server.IOptionsServer;

/**
 * @author yzhou-citrix
 *
 */
public class RepositoryDeployer {

    private static Logger logger = LoggerFactory.getLogger(RepositoryDeployer.class);
    
    // Repository need to be deployed
    private Repository repo = null;
    
    // Test CI that repository need to be deployed
    private BeaconCI testCi;
    private CIInfo testCiInfo;
    
    // Production CI that repository need to be deployed
    private BeaconCI prodCi;
    private CIInfo prodCiInfo;
    
    // Platform perforce server that host config and tools
    private IOptionsServer perforceServer = null;
    
    public RepositoryDeployer() {
    }
    
    public RepositoryDeployer(CIInfo testCIInfo) {
        this();
        this.testCiInfo = testCIInfo;
        this.testCi = new BeaconCI(testCIInfo);
    } 
    
    
    public RepositoryDeployer(CIInfo testCIInfo, CIInfo prodCIInfo) {
        this(testCIInfo);
        this.prodCiInfo = prodCIInfo;
        this.prodCi = new BeaconCI(prodCIInfo);
    }

    /** Prepare resources needed for deploying repositories on TEST CI
     * @return boolean: if prepare all resources successfully 
     */
    public boolean prepareTestDeploy() {
        logger.info("Perparing for test deploy ...");
        
        String server = Config.me().getProperty("platform.perforce.server");
        String userName = Config.me().getProperty("platform.perforce.username");
        String password = PerforcePasswordEncryptor.instance().decryptString(Config.me().getProperty("platform.perforce.password"));
        
        logger.info("Init connection to platform perforce server: {} ...", server);
        perforceServer = PerforceKit.getPerforceServer(server, userName, password);
        if(perforceServer == null) {
            logger.error("Fail to connect to platform perforce server: {}", server);
            return false;
        }
 
        return true;
    }
    
    
    private void doCheckConfigFile() {
        // check if tools' config files of repository have been checked into platform server
        logger.info("Check if config file exists ...");
        String rootPath = Config.me().getProperty("test.config.root", "//localization/LocEngineering/LocEngg-Tools/BeaconToolConfig_TEST/");
        if(!isExistConfigFile(rootPath)) {
            logger.error("Config file for {} does not exist in {}/{}", repo.getLabel(), rootPath, repo.getPath());
            // Check if the config file has been checked in prod root
            String prodRootPath = Config.me().getProperty("prod.config.root");
            if(isExistConfigFile(prodRootPath)) { 
                logger.info("Please copy it from {}/{}", prodRootPath, repo.getPath()); //TODO: auto copy files to there
            } else {
                logger.error("Please contact dev owner to check in config files");
            }
            throw new RuntimeException();
        }
        
    }
    
    /** Check provided source code information correct before deploy
     * 
     */
    private void doCheckSourcCode() {
        logger.info("Check if source view map correct ...");
        
        switch (repo.getScm().getType()) {
        case SCM_TYPE_PERFORCE:
            doCheckPerforeViewMap();
            break;
        case SCM_TYPE_GIT:
            
            break;
        default:
            logger.error("Scm Type {} is not supported", repo.getScm().getType());
        } 
    }
    
    private IOptionsServer getPerforceSourceServer(Perforce perforce) {
        
        if(perforce.getPort().equals("1111")) { // when perforce port is 1111, just use platform perforce server
            return perforceServer;
        } else {
            logger.info("Connecting perforce source server: {}", perforce.getServer());
            IOptionsServer srcServer = PerforceKit.getPerforceServer(perforce.getServer(), 
                    Config.me().getProperty("citrix.perforce.username"), 
                    PerforcePasswordEncryptor.instance().decryptString(Config.me().getProperty("citrix.perforce.password")));
            
            if(srcServer == null) {
                throw new RuntimeException(String.format("Could not connect to perforce source server: %s", perforce.getServer()));
            }
            
            return srcServer;
        }
    }
    private void doCheckPerforeViewMap() {
        Perforce perforce = (Perforce)repo.getScm();
        
        final IOptionsServer srcServer = getPerforceSourceServer(perforce);

        boolean pass = perforce.getViewMap().keySet().stream()
        .map(path -> {
            logger.info("Validating path: {}", path);
            
            return PerforceKit.validatePath(srcServer, path);
        })
        .reduce((r1,r2) -> r1 && r2).get();
        
        if(!pass) {
            throw new RuntimeException("Fail to check perfore view map");
        }
        
        if(srcServer != null && srcServer != perforceServer) {
            PerforceKit.releasePerforceServer(srcServer);
        }
    }
    
    
    /** Deploy repository: repo to TEST CI
     * @param repo 
     */
    public boolean doTestDeploy(boolean force, Repository repo){
        
        if(repo == null) return false;
        this.repo = repo;
        
        logger.info("Deploying repository: {} ...", repo.getName());
        
        try {
            
            doCheckConfigFile();
            
            doCheckSourcCode();
            
            // Deploy jobs into to Beacon Test CI
            logger.info("Deploying jobs ...");
            deployJobs(force);
            
        } catch (Exception e) {
            logger.error("Fail to deploy: {}", repo.getName(), e);
            return false;
        }
        finally {
            
        }
        
        logger.info("Test Deploy {} over", repo.getName());
        
        return true;
    }
    
    /** Release resources after complete deploying repositories on TEST CI
     * @return void
     */
    public void completeTestDeploy() {
        
        logger.info("Completing for test deploy ...");
        
        if(perforceServer != null) {
            logger.info("Release connection to platform perforce server ...");
            PerforceKit.releasePerforceServer(perforceServer);
        }
    }
    
    private boolean isExistConfigFile(String rootPath) {
        
        String configPath = rootPath;
        if(!rootPath.endsWith("/")) {
            configPath += "/";
        }
        configPath += repo.getPath() + "...";
        
        try {
            List<IFileSpec> fileList = perforceServer.getDepotFiles(FileSpecBuilder.makeFileSpecList(configPath), false);
            if(fileList != null && fileList.size() > 0 &&
                fileList.stream().filter(f -> f.getOpStatus() == FileSpecOpStatus.VALID 
                && f.getDepotPathString().endsWith("string_detector.config")).count() > 0) {
                return true;
            }
            
        } catch (Exception e) {
            logger.error("List file fail: ", e);
        } 
        
        return false;
    }
    
    public Document getTempDoc(String tempName) {
        String srcTempPath = tempPath + tempName;
        return PerforceKit.getXmlAsDoc(perforceServer, srcTempPath);
    }

    public Repository getRepo() {
        return repo;
    }
    

    public void setRepo(Repository repo) {
        this.repo = repo;
    }
    

    public BeaconCI getTestCi() {
        return testCi;
    }
    

    public void setTestCi(BeaconCI testCi) {
        this.testCi = testCi;
    }
    

    public CIInfo getTestCiInfo() {
        return testCiInfo;
    }
    

    public void setTestCiInfo(CIInfo testCiInfo) {
        this.testCiInfo = testCiInfo;
    }
    

    public BeaconCI getProdCi() {
        return prodCi;
    }
    

    public void setProdCi(BeaconCI prodCi) {
        this.prodCi = prodCi;
    }
    

    public CIInfo getProdCiInfo() {
        return prodCiInfo;
    }
    

    public void setProdCiInfo(CIInfo prodCiInfo) {
        this.prodCiInfo = prodCiInfo;
    }
    

    public String getTempPath() {
        return tempPath;
    }
    

    public void setTempPath(String tempPath) {
        this.tempPath = tempPath;
    }
    
    private String tempPath; // job templates' perforce path
    private void deployJobs(boolean force){
        List<BeaconJob> jobList = Arrays.asList(new BeaconSrcJob(this), 
                new BeaconConfigJob(this), 
                new BeaconToolsetJob(this),
                new BeaconDataImportJob(this));
        
        List<BeaconJob> toolJobList = repo.getTools().stream().map(t -> {
        	return new BeaconToolJob(this, ToolType.valueOf(t));
        }).collect(Collectors.toList());
        
        jobList = Stream.concat(jobList.stream(), toolJobList.stream()).collect(Collectors.toList());
        
        tempPath = "";
        switch (repo.getScm().getType()) {
        case SCM_TYPE_PERFORCE:
            tempPath = Config.me().getProperty("test.perforce.temp", "//localization/LocEngineering/LocEngg-Tools/BeaconBackendTools/auto_deployment_scripts/BeaconTestCIJobManagement/templates/");
            break;
        case SCM_TYPE_GIT:
            tempPath = Config.me().getProperty("test.git.temp", "//localization/LocEngineering/LocEngg-Tools/BeaconBackendTools/auto_deployment_scripts/BeaconTestCIJobManagementGit/templates/");
            break;
        default:
            logger.error("Scm Type {} is not supported", repo.getScm().getType());
            throw new RuntimeException(String.format("Scm Type %s is not supported", repo.getScm().getType()));
        }
        
        if(force) {
            jobList.forEach(BeaconJob::delete);
        }
        
        jobList.forEach(job -> {
            Deployable deployJob = (Deployable)job;
            deployJob.add();
        });
    }
    
    /*
     * Deploy repository to prod ci
     * */
    
    public boolean doProdDeploy(Repository repo, boolean force) {
        
        if(repo != null) this.repo = repo;
        
        return prodDeploy(force);
    }
    
    public void prodDeploy() {
        prodDeploy(false);
    }
    
    public boolean prodDeploy(boolean force){
        
        logger.info("Deploying repository: {} to production ci: {}", repo.getName(), prodCi.getHostURL());

        List<String> jobsExist = prodCi.getJobNames(repo.getName());
        
        if(jobsExist.size() > 0) {
            if(force) {
                jobsExist.stream().forEach(jobName -> {
                    try {
                        logger.info("Deleting job {} from ci {}", jobName, prodCi.getHostURL());
                        prodCi.deleteJob(jobName,false);
                        logger.info("Deleted job {}", jobName);
                    } catch (Exception e) {
                        logger.error("Fail to delete job {} from ci {}", jobName, prodCi.getHostURL());
                    }
                });
            }
            else {
                logger.error("Some jobs for repository {} has been on prod ci: {}, see list", repo.getName(), prodCi.getHostURL());
                jobsExist.forEach(logger::error);
                return false;
            }
        }
        
        // get all the jobs of repostiroy in test ci
        List<String> jobsInTest = testCi.getJobNames(repo.getName());
        
        if(jobsInTest.size() == 0) {
            logger.error("No jobs found for repository {} on test ci: {}", repo.getName(), testCi.getHostURL());
            return false;
        }
        
        // check all the jobs has been build successfully on TEST CI
        boolean testStatus = jobsInTest.stream().map(jobName -> {
            try {
                BuildResult buildResult = testCi.getJob(jobName).getLastBuild().details().getResult();
                
                if(buildResult != BuildResult.SUCCESS) {
                    logger.error("Last build result of job: {} on test ic: {} is {}", jobName, testCi.getHostURL(), buildResult);
                } else {
                    return true;
                }
                
            } catch (Exception e) {
                logger.error("Fail to check last build result of job: {}", jobName, e);
            }
            
            return false;
        }).reduce((r1,r2) -> r1 && r2).get();
        
        if(!testStatus) return false;
        
        
        jobsInTest.stream().map(jobName -> {
            BeaconJob job = null;
            String jobType = jobName.split("-")[0];
            logger.info("Map {} to {} job", jobName, jobType);
            if(jobType.equalsIgnoreCase(BeaconJobType.Src.name())) { // 
                job = new BeaconSrcJob(this);
            } else if(jobType.equalsIgnoreCase(BeaconJobType.Config.name())) {
                job = new BeaconConfigJob(this);
            } else if(jobType.equalsIgnoreCase(BeaconJobType.Toolset.name())) {
                job = new BeaconToolsetJob(this);
            } else if(jobType.equalsIgnoreCase(BeaconJobType.DataImporter.name())) {
                job = new BeaconDataImportJob(this);
            } else { //tool
                try {
                    job = new BeaconToolJob(this, ToolType.valueOf(jobType));
                } catch (Exception e) {
                    logger.error("Unknow job type or tool type",e);
                }
            }
            return job;
        }).forEach(job -> {
            Deployable deployJob = (Deployable)job;
            deployJob.move();
        });

        logger.info("Deployed repository: {} to production ci: {}", repo.getName(), prodCi.getHostURL());
        
        return true;
        
    }
}
